﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Web;
using System.Web.Caching;

using System.Runtime.Serialization.Formatters.Binary;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;

// Based on http://weblogs.asp.net/gunnarpeipman/archive/2009/11/19/asp-net-4-0-writing-custom-output-cache-providers.aspx

namespace TallComponents.Demo.Cache
{
   /// <summary>
   /// Caches data to disk.
   /// </summary>
   public class DiskCacheProvider : OutputCacheProvider
   {
      private string _cachePath;

      private string CachePath
      {
         get
         {
            if ( !string.IsNullOrEmpty( _cachePath ) )
               return _cachePath;

            _cachePath = ConfigurationManager.AppSettings[ "OutputCachePath" ];
            if ( string.IsNullOrWhiteSpace( _cachePath ) )
            {
               throw new InvalidOperationException( "The AppSetting 'OutputCachePath' is not specified or invalid." );
            }

            var context = HttpContext.Current;

            if ( context != null )
            {
               _cachePath = context.Server.MapPath( _cachePath );
               if ( !_cachePath.EndsWith( "\\" ) )
                  _cachePath += "\\";
            }

            return _cachePath;
         }
      }

      /// <summary>
      /// Inserts the specified entry into the output cache.
      /// </summary>
      /// <param name="key">A unique identifier for <paramref name="entry"/>.</param>
      /// <param name="entry">The content to add to the output cache.</param>
      /// <param name="utcExpiry">The time and date on which the cached entry expires.</param>
      /// <returns>A reference to the specified provider.</returns>
      public override object Add( string key, object entry, DateTime utcExpiry )
      {
         Debug.WriteLine( "Cache.Add(" + key + ", " + entry + ", " + utcExpiry + ")" );

         var path = GetPathFromKey( key );

         if ( File.Exists( path ) )
            return entry;

         if ( !Directory.Exists( Path.GetDirectoryName( path ) ) )
         {
            throw new InvalidOperationException(
               "Output caching directory doesn't exist. Please create it first and make sure the application hass sufficient rights to write to it." );
         }
         try
         {
            using ( var file = File.OpenWrite( path ) )
            {
               var item = new CacheItem { Expires = utcExpiry, Item = entry };
               var formatter = new BinaryFormatter();
               formatter.Serialize( file, item );
            }
         }
         catch ( Exception ex )
         {
            Debug.WriteLine( "Cache ERROR: " + ex.ToString() );

         }

         return entry;
      }

      /// <summary>
      /// Returns a reference to the specified entry in the output cache.
      /// </summary>
      /// <param name="key">A unique identifier for a cached entry in the output cache.</param>
      /// <returns>
      /// The <paramref name="key"/> value that identifies the specified entry in the cache, or null if the specified entry is not in the cache.
      /// </returns>
      public override object Get( string key )
      {
         CacheItem item = null;

         var path = GetPathFromKey( key );

         if ( File.Exists( path ) )
         {
            using (var file = File.OpenRead( path ))
            {
               var formatter = new BinaryFormatter();
               item = (CacheItem) formatter.Deserialize( file );
            }

            if ( item != null && item.Expires <= DateTime.Now.ToUniversalTime() )
            {
               Debug.WriteLine( "Expired: {0}", key );
               Remove( key );
            }
         }

         UpdateStats( key, item != null && item.Item != null );
         return item != null ? item.Item : null;
      }

      /// <summary>
      /// Removes the specified entry from the output cache.
      /// </summary>
      /// <param name="key">The unique identifier for the entry to remove from the output cache.</param>
      public override void Remove( string key )
      {
         Debug.WriteLine( "Cache.Remove(" + key + ")" );

         var path = GetPathFromKey( key );

         if ( File.Exists( path ) )
            File.Delete( path );
      }

      /// <summary>
      /// Inserts the specified entry into the output cache, overwriting the entry if it is already cached.
      /// </summary>
      /// <param name="key">A unique identifier for <paramref name="entry"/>.</param>
      /// <param name="entry">The content to add to the output cache.</param>
      /// <param name="utcExpiry">The time and date on which the cached <paramref name="entry"/> expires.</param>
      public override void Set( string key, object entry, DateTime utcExpiry )
      {
         Debug.WriteLine( "Cache.Set(" + key + ", " + entry + ", " + utcExpiry + ")" );

         var item = new CacheItem { Expires = utcExpiry, Item = entry };
         var path = GetPathFromKey( key );

         using ( var file = File.OpenWrite( path ) )
         {
            var formatter = new BinaryFormatter();
            formatter.Serialize( file, item );
         }
      }

      private string GetPathFromKey( string key )
      {
         return CachePath + MD5( key ) + ".txt";
      }

      private string MD5( string s )
      {
         MD5CryptoServiceProvider provider;
         provider = new MD5CryptoServiceProvider();
         byte[] bytes = Encoding.UTF8.GetBytes( s );
         StringBuilder builder = new StringBuilder();

         bytes = provider.ComputeHash( bytes );

         foreach ( byte b in bytes )
            builder.Append( b.ToString( "x2" ).ToLower() );

         return builder.ToString();
      }

      private void UpdateStats( string key, bool hit )
      {
         CacheStats stats;
         if( !_stats.TryGetValue( key, out stats ))
         {
            stats = new CacheStats();
            _stats.Add( key, stats );
         }
         if( hit )
         {
            stats.Hits++;
         }
         else
         {
            stats.Misses++;
         }
         Debug.WriteLine( "{0} Hits : {1} - Misses : {2}", key, stats.Hits, stats.Misses  );
      }
      private class CacheStats
      {
         public int Hits { get; set; }
         public int Misses { get; set; }
      }
      private Dictionary<string, CacheStats> _stats = new Dictionary<string, CacheStats>();
   }
}
